#include <iostream>
#include "FileCopier.h"
#include "FileOperations.h"
#include <stdlib.h>
#include <string.h>
#include <stack>
#include "SortedTree.h"
#include <math.h>

int FileCopier::RunCopier(int argc, gchar* argv[], double version)
{
    Options options;
    int loop;
    gstring argument;
    gstring source;
    gstring target;

    if (argc > 2)
    {
        for (loop = 1; loop < argc; loop++)
        {
            argument = argv[loop];
            if (argument == TEXT("-L"))
            {
                options.DontCopyFiles = true;
                continue;
            }
            if (argument == TEXT("-D"))
            {
                options.DeleteUnusedFiles = true;
                continue;
            }
            if (argument == TEXT("-T"))
            {
                options.TestReadingDirectory = true;
                continue;
            }
            if (argument == TEXT("-S"))
            {
                loop++;
                options.Speed = gtoi(argv[loop]);
                if ((options.Speed < 1) || (options.Speed > 100))
                {
                    options.Speed = 100;
                }
                continue;
            }
            if (argument == TEXT("-M"))
            {
                options.LookForMovedFiles = true;
                continue;
            }
            if (source.size() == 0)
            {
                source = argument;
                continue;
            }
            if (target.size() == 0)
            {
                target = argument;
                continue;
            }
            source.clear();
            target.clear();
            break;
        }        
        if ((source.size() > 0) && options.TestReadingDirectory)
        {
            RunCopier(source, source, options);
        } else if ((target.size() > 0) && !options.TestReadingDirectory)
        {
            RunCopier(source, target, options);
        } else {
            cout << "Error: Unrecognized options.\n";
        }
    }
    else
    {
        cout << "FileCopier version " << version << "\n";
        cout << "Copies files from one folder to another. Good for doing backups.\n";
        cout << "FileCopier source destination\n";
        cout << "Copies all the source files to the destination directory.\n";
        cout << "Only changed files are copied.\n";
        cout << "Extra files in the destination are not deleted.\n";
        cout << "Example:\n";
        if (FileOperations::DirectorySeparator() == '\\')
        {
            cout << "FileCopier \"C:\\Games\" M:\\\n";
            cout << "FileCopier -L C:\\Games M:\\\n";
        }
        else
        {
            cout << "FileCopier \"\\home\\user\\Music Folder\" \\home\\user\\backups\n";
            cout << "FileCopier -L \\home\\user\\Songs \\home\\user\\OldSongs\n";
        }        
        cout << "Options:\n";
        cout << " -L Only list the changes. Don't copy or delete files.\n";
        cout << " -D Delete files in the target that aren't in the source.\n";
        cout << " -S number Set the copy speed from 1 to 100 with 100 being the fastest.\n";
        cout << " -M Look for moved files to move them instead of copy them.\n";
        cout << " -T Test reading the directory.\n";
    }
    cout << "Press enter.\n";
    return getchar() * 0; // This is to prevent a compiler warning of ignoring the return value of getchar().
}

void FileCopier::RunCopier(const gstring& source, const gstring& target, Options& options)
{
    int howManyFilesCopied = 0;
    INT64 howManyBytesCopied = 0;
    size_t numberOfFilesMoved = 0;
    INT64 howManyFilesDeleted = 0;
    if (!FileOperations::DoesFolderExist(source))
    {
        cout << "source folder ";
        FileOperations::Print(source);
        cout << " wasn't found.\n";
        return;
    }
    if (!options.TestReadingDirectory && !FileOperations::DoesFolderExist(target))
    {
        FileOperations::DirectoryCreate(target);
        if (!FileOperations::DoesFolderExist(target))
        {
            cout << "target folder ";
            FileOperations::Print(target);
            cout << " wasn't found and could not be created.\n";
        }
    }
    if (options.TestReadingDirectory)
    {
        cout << "File Name:File Size:Last Modified Date/Time\n";
    } else
    {
        cout << "Copying from ";
        FileOperations::Print(source);
        cout << " to ";
        FileOperations::Print(target);
        cout << "\n";
    }    
    if (options.LookForMovedFiles && !options.TestReadingDirectory)
    {
        numberOfFilesMoved = FindFilesToMoveAndMoveThem(source, target, howManyFilesDeleted, options);
    }

    RunCopier(source, target, 0, 100, howManyFilesCopied, howManyBytesCopied, howManyFilesDeleted, options);
    FileOperations::Print(howManyFilesCopied);
    cout << " file" << (howManyFilesCopied == 1 ? "" : "s") << " copied. ";
    FileOperations::Print(howManyBytesCopied);
    cout << " byte" << (howManyBytesCopied == 1 ? "" : "s") << " copied. ";
    FileOperations::Print(numberOfFilesMoved);
    cout << " file" << (numberOfFilesMoved == 1 ? "" : "s") << " moved. ";
    FileOperations::Print(howManyFilesDeleted);
    cout << " file" << (howManyFilesDeleted == 1 ? "" : "s") << " deleted.\n";
}

bool FileCopier::RunCopier(const gstring &source, const gstring &target, double startPercentComplete,
    double endPercentComplete, int& howManyFilesCopied, INT64& howManyBytesCopied, 
    INT64& howManyFilesDeleted, Options& options)
{
    DirectoryListing sourceListing;
    DirectoryListing targetListing;
    gstring tempSource;    
    double percentAdd;
    int directoryCount;
    bool createTargetDirectory;
    DirectoryEntry* targetFile;
    gstring tempTarget;

    sourceListing.ReadDirectory(source);
    if (!options.TestReadingDirectory)
    {
        targetListing.ReadDirectory(target);
    }
    // Delete missing files and directories.
    if (options.DeleteUnusedFiles && !options.TestReadingDirectory) {
        for (auto& targetItem : targetListing.Listing)
        {
            if (!targetItem->IsHidden() && !targetItem->IsSystem() && !targetItem->IsReadOnly())
            {
                if (sourceListing.FindFile(targetItem->FileName) == NULL)
                {
                    // Target file wasn't found in the source directory. Delete it.                    
                    DirectoryListing::CombineFileNames(target, targetItem->FileName, tempTarget);
                    if (targetItem->IsDirectory())
                    {
                        DeleteDirectoryAndEverythingInIt(tempTarget, howManyFilesDeleted, options);
                    }
                    else
                    {
                        FileOperations::Print(tempTarget);
                        cout << " deleted.\n";
                        if (!options.DontCopyFiles)
                        {
                            FileOperations::FileDelete(tempTarget);
                            howManyFilesDeleted++;
                        }
                    }
                }
            }
        }        
    }
    // Count the directories.
    directoryCount = 0;
    for (auto& sourceItem : sourceListing.Listing)
    {
        if (sourceItem->IsDirectory() && !sourceItem->IsHidden() && !sourceItem->IsSystem())
        {
            directoryCount++;
        }        
    }
    percentAdd = (endPercentComplete - startPercentComplete) / (directoryCount + 1);
    // Copy all the changed files in this directory.
    if (!CopyChangedFiles(source, sourceListing, target, targetListing, startPercentComplete, startPercentComplete + percentAdd, howManyFilesCopied, howManyBytesCopied, options))
    {
        return false;
    }
    // Copy all the subdirectories.
    if (directoryCount > 0)
    {
        startPercentComplete += percentAdd;
        // Loop through all the directories.
        for (auto& sourceItem : sourceListing.Listing)
        {
            if (sourceItem->IsDirectory() && !sourceItem->IsHidden() && !sourceItem->IsSystem())
            {
                DirectoryListing::CombineFileNames(source, sourceItem->FileName, tempSource);
                if (!options.TestReadingDirectory)
                {
                    DirectoryListing::CombineFileNames(target, sourceItem->FileName, tempTarget);
                    targetFile = targetListing.FindFile(sourceItem->FileName);
                    createTargetDirectory = (targetFile == NULL);
                    if ((!createTargetDirectory) && (!targetFile->IsDirectory()) && (!options.DontCopyFiles))
                    {
                        // Delete this file because a directory goes here.
                        FileOperations::FileDelete(tempTarget);
                        howManyFilesDeleted++;
                        createTargetDirectory = true;
                    }
                    if (createTargetDirectory && (!options.DontCopyFiles))
                    {
                        FileOperations::DirectoryCreate(tempTarget);
                    }
                }
                if (!RunCopier(tempSource, tempTarget, startPercentComplete, startPercentComplete + percentAdd, howManyFilesCopied, howManyBytesCopied, howManyFilesDeleted, options))
                {
                    return false;
                }
                startPercentComplete += percentAdd;
            }
        }
    }    
    return true;
}

bool FileCopier::CopyChangedFiles(const gstring& source, DirectoryListing& sourceListing, const gstring& target, DirectoryListing& targetListing, double startPercentComplete, double endPercentComplete, int& howManyFilesCopied, INT64& howManyBytesCopied, Options& options)
{
    gstring sourceFullName;
    gstring targetFullName;
    DirectoryEntry* targetFile;
    bool shouldCopyFile;
    double percentAdd;
    int fileCount;
    bool copied;
    gstring fileDate;

    // Count the number of files.
    fileCount = 0;
    for (auto& sourceFile : sourceListing.Listing)
    {
        if (!sourceFile->IsDirectory() && !sourceFile->IsHidden() && !sourceFile->IsSystem())
        {
            fileCount++;
        }
    }
    if (fileCount == 0)
    {
        return true;
    }
    percentAdd = (endPercentComplete - startPercentComplete) / fileCount;
    for (auto& sourceFile : sourceListing.Listing)
    {
        if (!sourceFile->IsDirectory() && !sourceFile->IsHidden() && !sourceFile->IsSystem())
        {
            startPercentComplete += percentAdd;
            if (options.TestReadingDirectory)
            {
                FileOperations::Print(sourceFile->FileName);
                cout << ":";
                FileOperations::Print(sourceFile->nFileSize);
                FileOperations::FileDateToString(sourceFile->LastWriteTime, fileDate);
                cout << ":";
                FileOperations::Print(fileDate);
                cout << "\n";
            }
            else
            {
                shouldCopyFile = true;
                // Look for the target file to see if it's found.
                targetFile = targetListing.FindFile(sourceFile->FileName);
                if (targetFile != NULL)
                {
                    if ((sourceFile->nFileSize == targetFile->nFileSize) && (sourceFile->LastWriteTime <= targetFile->LastWriteTime))
                    {
                        // The file hasn't changed.
                        shouldCopyFile = false;
                    }
                }
                copied = true;
                if (shouldCopyFile)
                {
                    DirectoryListing::CombineFileNames(source, sourceFile->FileName, sourceFullName);
                    cout << trunc(startPercentComplete * 100) / 100 << " complete copying ";
                    FileOperations::Print(sourceFullName);
                    cout << "\n";
                    //printf("%.2f%% complete copying %ws\n", trunc(startPercentComplete * 100) / 100, sourceFullName.c_str());
                    if (!options.DontCopyFiles)
                    {
                        DirectoryListing::CombineFileNames(target, sourceFile->FileName, targetFullName);
                        copied = FileOperations::CopyTheFile(sourceFullName, targetFullName, options.Speed);
                    }
                    howManyFilesCopied++;
                    howManyBytesCopied += sourceFile->nFileSize;
                }
                if (!copied)
                {
                    return false;
                }
            }            
        }
    }    
    return true;
}

bool FileCopier::DeleteDirectoryAndEverythingInIt(const gstring& directory, INT64& howManyFilesDeleted, Options& options)
{
    DirectoryListing* listing;
    gstring targetFullName;    
    listing = new DirectoryListing();
    bool deletedAllFiles;
    listing->ReadDirectory(directory);
    // Delete the files in this directory.
    deletedAllFiles = true;
    for (auto& file : listing->Listing)
    {
        if (file->IsReadOnly() || file->IsHidden() || file->IsSystem())
        {
            // This file or directory can't be deleted.
            deletedAllFiles = false;
        }
        else
        {
            if (file->IsDirectory())
            {
                // Delete this directory.
                DirectoryListing::CombineFileNames(directory, file->FileName, targetFullName);
                deletedAllFiles = deletedAllFiles && DeleteDirectoryAndEverythingInIt(targetFullName, howManyFilesDeleted,  options);
            }
            else
            {
                // Delete this file.
                DirectoryListing::CombineFileNames(directory, file->FileName, targetFullName);
                FileOperations::Print(targetFullName);
                cout << " deleted.\n";
                if (!options.DontCopyFiles)
                {
                    deletedAllFiles = deletedAllFiles && FileOperations::FileDelete(targetFullName);
                    howManyFilesDeleted++;
                }
            }
        }
    }
    if (deletedAllFiles)
    {
        // delete this empty directory.
        FileOperations::Print(directory);
        cout << " deleted.\n";
        if (!options.DontCopyFiles)
        {
            deletedAllFiles = deletedAllFiles && FileOperations::DirectoryDelete(directory);
        }
    }
    delete listing;
    return deletedAllFiles;
}

size_t FileCopier::FindFilesToMoveAndMoveThem(const gstring& source, const gstring& target, INT64& howManyFilesDeleted, Options& options)
{
    // Look for files that should be renamed or moved.
    size_t numberOfFilesMoved;
    DirectoryTree sourceTree;
    SortedTree< DirectoryEntryWithTree> *sourceSorted = NULL;
    DirectoryEntryWithTree* entryWithTree;
    DirectoryTree* treeLoop;
    DirectoryTree targetTree;
    list<FileToMove*>itemsToMove;
    DirectoryEntryWithTree *search;    
    SortedTree<DirectoryEntryWithTree>* searchResults;
    stack<DirectoryTree*>flatten; // This stack is used to flatten a DirectoryTree.
    FileToMove* fileToMove;
    int fileCount;
    // The source and target folders could be read at the same time in two threads. But this conflicts with the idea of running slow in the background.
    cout << "Reading directory ";
    FileOperations::Print(source);
    cout << "\n";
    sourceTree.Folder = source;
    sourceTree.Speed = options.Speed;
    sourceTree.ReadDirectory(true);
    fileCount = sourceTree.FileCount(true);
    if (fileCount == 0)
    {
        return 0;
    }
    cout << "Source directory has ";
    FileOperations::Print(fileCount);
    cout << " files. Reading directory ";
    FileOperations::Print(target);
    cout << "\n";
    targetTree.Folder = target;
    targetTree.Speed = options.Speed;
    targetTree.ReadDirectory(true);
    fileCount = targetTree.FileCount(true);
    if (fileCount == 0)
    {
        return 0;
    }
    cout << "Target directory has ";
    FileOperations::Print(fileCount);
    cout << " files. Looking for files to move.\n";
    // Add all the items in sourceTree to sourceSorted.
    flatten.push(&sourceTree);
    while (!flatten.empty())
    {
        treeLoop = flatten.top();
        flatten.pop();
        for (auto& file : treeLoop->Files)
        {
            entryWithTree = new DirectoryEntryWithTree();
            entryWithTree->File = file;
            entryWithTree->Folder = treeLoop;
            if (sourceSorted == NULL)
            {
                sourceSorted = new SortedTree< DirectoryEntryWithTree>(true);
                sourceSorted->Items.push_back(entryWithTree);
            }
            else
            {
                sourceSorted->AddItem(entryWithTree, DirectoryEntryWithTree::CompareFiles);
            }            
            entryWithTree = NULL;
        }
        for (auto& folder : treeLoop->Folders)
        {
            flatten.push(folder);
        }
    }
    // Search each target for a source match.
    search = NULL;
    flatten.push(&targetTree);
    while (!flatten.empty())
    {
        treeLoop = flatten.top();
        flatten.pop();
        if (search == NULL)
        {
            search = new DirectoryEntryWithTree();
        }
        search->Folder = treeLoop;
        for (auto& file : treeLoop->Files)
        {
            if (search == NULL)
            {
                search = new DirectoryEntryWithTree();
                search->Folder = treeLoop;
            }
            search->File = file;
            searchResults = sourceSorted->FindItem(search, DirectoryEntryWithTree::CompareFiles);
            if ((searchResults != NULL) && (searchResults->Items.size() == 1))
            {
                if (!search->AreFoldersTheSame(searchResults->Items.front()))
                {
                    // This file is in the same name, but in a different folder. Move it.                
                    fileToMove = new FileToMove();
                    fileToMove->SourceItem = searchResults->Items.front(); 
                    fileToMove->TargetItem = search; // This item will be deleted in MoveFiles.
                    itemsToMove.push_back(fileToMove);
                    search = NULL;
                }
            }
        }
        for (auto& folder : treeLoop->Folders)
        {
            flatten.push(folder);
        }
    }
    if (search != NULL)
    {
        search->File = NULL;
        search->Folder = NULL;
        delete search;
        search = NULL;
    }
    cout << "Found ";
    numberOfFilesMoved = itemsToMove.size();
    FileOperations::Print(numberOfFilesMoved);
    cout << " files to move.\n";
    MoveFiles(itemsToMove, howManyFilesDeleted, options);
    delete sourceSorted;
    sourceSorted = NULL;
    return numberOfFilesMoved;
}
void FileCopier::MoveFiles(list<FileToMove*>&filesToMove, INT64& howManyFilesDeleted, Options& options)
{
    FileToMove* fileToMove;
    gstring currentTargetPath;
    gstring newTargetPath;
    SortedList<gstring> *foldersCreated = NULL; // List of folders created so far. This is used to prevent trying to create a folder twice.
    currentTargetPath.reserve(MAX_PATH);
    newTargetPath.reserve(MAX_PATH);
    FileAttributes fileInfo;
    while (!filesToMove.empty())
    {
        fileToMove = filesToMove.front();
        filesToMove.pop_front();
        if ((fileToMove == NULL) || (fileToMove->TargetItem == NULL)) // This should never happen. It's put in here to prevent compiler warnings of a possible NULL reference error.
        {
            continue;
        }
        
        // File to move.
        fileToMove->TargetItem->Folder->GetFullPath(&(fileToMove->TargetItem->File->FileName), currentTargetPath);
        // New location. Take the folders in the source and combine it with the root folder in target to create a new target location.
        CalculateNewPathForMovedFile(fileToMove->SourceItem, fileToMove->TargetItem, newTargetPath);
        // Delete the newTargetPath file/directory if it exists.
        if (fileInfo.GetAttributes(newTargetPath))
        {
            // The target file already exists.
            if (fileInfo.IsDirectory())
            {
                DeleteDirectoryAndEverythingInIt(newTargetPath, howManyFilesDeleted, options);
            }
            else
            {
                if (!options.DontCopyFiles)
                {
                    FileOperations::FileDelete(newTargetPath);
                    howManyFilesDeleted++;
                }                
            }
        }
        MoveFileCreateTargetFolder(currentTargetPath, newTargetPath, &foldersCreated, options);
        // Delete fileToMove->TargetItem because it was created in FindFilesToMoveAndMoveThem.
        if (fileToMove->TargetItem != NULL)
        {
            fileToMove->TargetItem->File = NULL;
            fileToMove->TargetItem->Folder = NULL;
            delete fileToMove->TargetItem;
            fileToMove->TargetItem = NULL;
        }
        delete fileToMove;
        fileToMove = NULL;
    }
    // Free foldersCreated.
    if (foldersCreated != NULL)
    {
        delete foldersCreated;
        foldersCreated = NULL;
    }
}
void FileCopier::CalculateNewPathForMovedFile(DirectoryEntryWithTree *source, DirectoryEntryWithTree* target, gstring &newTargetPath)
{
    DirectoryTree* grandParent;
    stack<gstring*>folders;
    gstring* folder;
    gchar directorySeparator = FileOperations::DirectorySeparator();
    // The root folder is the grand parent of target.
    grandParent = target->Folder->GrandParent();
    newTargetPath = grandParent->Folder;
    // Append all the folders of the source path except the grand parent.
    // Use a stack to reverse the order.
    grandParent = source->Folder;
    while (grandParent != NULL && grandParent->Parent != NULL)
    {
        folders.push(&(grandParent->Folder));
        grandParent = grandParent->Parent;
    }
    while (!folders.empty())
    {
        folder = folders.top();
        folders.pop();
        if (newTargetPath[newTargetPath.size() - 1] != directorySeparator)
        {
            newTargetPath += FileOperations::DirectorySeparator();
        }
        newTargetPath += *folder;
        folder = NULL;
    }
    if (newTargetPath[newTargetPath.size() - 1] != directorySeparator)
    {
        newTargetPath += directorySeparator;
    }
    newTargetPath += source->File->FileName;
}
void FileCopier::MoveFileCreateTargetFolder(const gstring& sourcePath, const gstring& targetPath, SortedList<gstring>** foldersCreated, Options& options)
{
    size_t search;
    gstring *folder;
    size_t targetLength = targetPath.size();
    bool found;
    bool folderExists = true;
    gchar directorySeparator = FileOperations::DirectorySeparator();
    folder = NULL;
    const bool AddIfNotAlreadyThere = true;
    for (search = 4; search < targetLength; search++)
    {
        if (targetPath[search] != directorySeparator)
        {
            continue;
        }
        if (folder == NULL)
        {
            folder = new gstring();
            folder->reserve(targetLength);
        }
        *folder = targetPath.substr(0, search);
        if (*foldersCreated == NULL)
        {
            *foldersCreated = new SortedList<gstring>(folder);
            found = false;
        }
        else
        {
            found = (*foldersCreated)->Find(folder, DirectoryEntry::CompareFileNames, AddIfNotAlreadyThere);
        }
        if (found)
        {
            continue;
        }
        if (!FileOperations::DoesFolderExist(*folder))
        {
            if (!options.DontCopyFiles)
            {
                FileOperations::DirectoryCreate(*folder);
                if (!FileOperations::DoesFolderExist(*folder))
                {
                    cout << "target folder ";
                    FileOperations::Print(*folder);
                    cout << " wasn't found and could not be created moving file ";
                    FileOperations::Print(sourcePath);
                    cout << ".\n";
                    folderExists = false;
                    folder = NULL; // folder is now stored in foldersCreated. It will be deleted from that class in the destructor.
                    break;
                }
            }
        }
        folder = NULL; // folder is now stored in foldersCreated. It will be deleted from that class in the destructor.
    }
    if (folder != NULL)
    {
        delete folder;
        folder = NULL;
    }
    cout << "Moving from ";
    FileOperations::Print(sourcePath);
    cout << " to ";
    FileOperations::Print(targetPath);
    cout << "\n";
    if (!options.DontCopyFiles)
    {
        FileOperations::MoveAFile(sourcePath, targetPath);        
    }
}

